<div class="draggable-area {draggableClassAfterRaf}"
     on:pointerMove="onPointerMove(event)"
     on:pointerLeave="onPointerLeave(event)"
     on:pointerUp="onPointerUp(event)"
     on:click="onClick(event)"
     ref:area
>
  <div class="draggable-indicator {computedIndicatorClassAfterRaf}"
       style={indicatorStyleAfterRaf}
       on:pointerDown="onPointerDown(event)"
       ref:indicator
  >
    <div class="draggable-indicator-inner">
      <slot></slot>
    </div>
  </div>
</div>
<style>
  .draggable-area {
    position: relative;
    touch-action: none;
    cursor: pointer;
  }
  .draggable-indicator {
    position: absolute;
    cursor: grab;
  }
  .draggable-indicator.grabbing {
    cursor: grabbing;
  }
  .draggable-indicator-inner {
    pointer-events: none;
    display: flex;
  }
</style>
<script>
  import { observe } from 'svelte-extras'
  import { throttleTimer } from '../_utils/throttleTimer.js'
  import { pointerUp, pointerDown, pointerLeave, pointerMove } from '../_utils/pointerEvents.js'
  import { requestPostAnimationFrame } from '../_utils/requestPostAnimationFrame.js'
  import { classname } from '../_utils/classname.js'

  // ensure DOM writes only happen once after a rAF
  const updateIndicatorStyle = throttleTimer(requestAnimationFrame)
  const updateIndicatorClass = throttleTimer(requestAnimationFrame)
  const updateDraggableClass = throttleTimer(requestAnimationFrame)

  // ensure DOM reads only happen once after a rPAF
  const calculateGBCR = throttleTimer(requestPostAnimationFrame)

  const clamp = x => Math.max(0, Math.min(1, x))

  export default {
    oncreate () {
      this.observe('dragging', dragging => {
        if (dragging) {
          this.fire('dragStart')
        } else {
          const { x, y } = this.get()
          this.fire('dragEnd')
          this.fire('change', { x, y })
        }
      }, { init: false })
      this.observe('indicatorStyle', () => {
        updateIndicatorStyle(() => {
          const { indicatorStyle } = this.get()
          this.set({ indicatorStyleAfterRaf: indicatorStyle })
        })
      })
      this.observe('computedIndicatorClass', () => {
        updateIndicatorClass(() => {
          const { computedIndicatorClass } = this.get()
          this.set(({ computedIndicatorClassAfterRaf: computedIndicatorClass }))
        })
      })
      this.observe('draggableClass', () => {
        updateDraggableClass(() => {
          const { draggableClass } = this.get()
          this.set({ draggableClassAfterRaf: draggableClass })
        })
      })
    },
    data: () => ({
      dragging: false,
      draggableClass: '',
      draggableClassAfterRaf: '',
      indicatorClass: '',
      computedIndicatorClassAfterRaf: '',
      x: 0,
      y: 0,
      indicatorWidth: 0,
      indicatorHeight: 0,
      indicatorStyleAfterRaf: ''
    }),
    computed: {
      indicatorStyle: ({ x, y, indicatorWidth, indicatorHeight }) => (
        `left: calc(${x * 100}% - ${indicatorWidth / 2}px); top: calc(${y * 100}% - ${indicatorHeight / 2}px);`
      ),
      computedIndicatorClass: ({ dragging, indicatorClass }) => classname(dragging && 'grabbing', indicatorClass)
    },
    methods: {
      observe,
      onPointerDown (e) {
        console.log('Draggable: onPointerDown')
        const rect = this.refs.indicator.getBoundingClientRect()
        console.log('Draggable: e.clientX', e.clientX)
        console.log('Draggable: e.clientY', e.clientY)
        this.set({
          dragging: true,
          dragOffsetX: e.clientX - rect.left,
          dragOffsetY: e.clientY - rect.top
        })
      },
      onPointerMove (e) {
        console.log('Draggable: onPointerMove')
        const { dragging, indicatorWidth, indicatorHeight, dragOffsetX, dragOffsetY } = this.get()
        if (dragging) {
          console.log('Draggable: dragging')
          calculateGBCR(() => {
            const rect = this.refs.area.getBoundingClientRect()
            const offsetX = dragOffsetX - (indicatorWidth / 2)
            const offsetY = dragOffsetY - (indicatorHeight / 2)
            const x = clamp((e.clientX - rect.left - offsetX) / rect.width)
            const y = clamp((e.clientY - rect.top - offsetY) / rect.height)
            this.set({ x, y })
          })
        }
      },
      onPointerUp (e) {
        console.log('Draggable: onPointerUp')
        this.set({ dragging: false })
      },
      onPointerLeave (e) {
        console.log('Draggable: onPointerLeave')
        this.set({ dragging: false })
      },
      onClick (e) {
        console.log('Draggable: onClick')
        console.log('Draggable: target classList', e.target.classList)
        console.log('Draggable: currentTarget classList', e.currentTarget.classList)
        if (!e.target.classList.contains('draggable-indicator')) {
          console.log('Draggable: onClick handled')
          const rect = this.refs.area.getBoundingClientRect()
          const x = clamp((e.clientX - rect.left) / rect.width)
          const y = clamp((e.clientY - rect.top) / rect.height)
          this.set({ x, y })
          this.fire('change', { x, y })
        }
      }
    },
    events: {
      pointerUp,
      pointerDown,
      pointerLeave,
      pointerMove
    }
  }
</script>
